package proxy;

import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;

import org.apache.http.ConnectionClosedException;
import org.apache.http.Header;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.DefaultHttpRequestFactory;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.entity.DisallowIdentityContentLengthStrategy;
import org.apache.http.impl.entity.EntityDeserializer;
import org.apache.http.impl.entity.EntitySerializer;
import org.apache.http.impl.entity.LaxContentLengthStrategy;
import org.apache.http.impl.entity.StrictContentLengthStrategy;
import org.apache.http.impl.io.DefaultHttpRequestParser;
import org.apache.http.impl.io.DefaultHttpResponseParser;
import org.apache.http.impl.io.HttpRequestWriter;
import org.apache.http.impl.io.HttpResponseWriter;
import org.apache.http.impl.io.SocketInputBuffer;
import org.apache.http.impl.io.SocketOutputBuffer;
import org.apache.http.io.HttpMessageParser;
import org.apache.http.io.HttpMessageWriter;
import org.apache.http.io.SessionInputBuffer;
import org.apache.http.io.SessionOutputBuffer;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.RequestConnControl;
import org.apache.http.protocol.RequestTargetHost;
import org.apache.http.protocol.ResponseConnControl;
import org.apache.http.protocol.ResponseContent;

import proxy.gui.HttpProxy;
import proxy.util.AuthenticationUtil;
import proxy.util.MatchUtil;
import proxy.util.RequestUtil;

public class HttpSession extends Thread {
	Socket clientSocket = null;
	Socket serverSocket = null;

	private HttpContext context = null;
	private HttpParams params = null;

	private BasicHttpProcessor processor;

	private DefaultHttpResponseFactory responseFactory = null;
	private DefaultHttpRequestFactory requestFactory = null;

	HttpProxy httpProxy = null;
	ConnectionDetail connection = null;

	private EntitySerializer entitySerializerClient;
	private EntityDeserializer entityDeserializerClient;
	private SessionInputBuffer clientIn = null;
	private SessionOutputBuffer clientOut = null;
	private HttpMessageParser<HttpRequest> requestParser = null;
	private HttpMessageWriter<HttpResponse> responseWriter = null;
	
	private HttpMessageParser<HttpResponse> responseParser = null;
	private HttpMessageWriter<HttpRequest> requestWriter = null;
	private SessionInputBuffer serverIn = null;
	private SessionOutputBuffer serverOut = null;
	private EntitySerializer entitySerializerServer;
	private EntityDeserializer entityDeserializerServer;
	
	private Log log;

	public HttpSession(Socket clientSocket, HttpProxy proxy, Log log) {
		this.clientSocket = clientSocket;
		this.httpProxy = proxy;
		this.log = log;

		context = new BasicHttpContext();
		params = new BasicHttpParams();

		responseFactory = new DefaultHttpResponseFactory();
		requestFactory = new DefaultHttpRequestFactory();

		processor = new BasicHttpProcessor();
		processor.addInterceptor(new RequestTargetHost());
		processor.addInterceptor(new RequestConnControl());
		processor.addResponseInterceptor(new ResponseConnControl());
		processor.addResponseInterceptor(new ResponseContent(true));

		try {
			clientIn = new SocketInputBuffer(clientSocket, -1, params);
			clientOut = new SocketOutputBuffer(clientSocket, -1, params);
		} catch (IOException e) {
			e.printStackTrace();
		}
		entitySerializerClient = new EntitySerializer(new StrictContentLengthStrategy());
		entityDeserializerClient = new EntityDeserializer(
				new DisallowIdentityContentLengthStrategy(
						new LaxContentLengthStrategy(0)));
		requestParser = new DefaultHttpRequestParser(clientIn, null, requestFactory, params);
		responseWriter = new HttpResponseWriter(clientOut, null, params);

		entitySerializerServer = new EntitySerializer(new StrictContentLengthStrategy());
		entityDeserializerServer = new EntityDeserializer(new LaxContentLengthStrategy());
	}

	public boolean isConnected() {
		return serverSocket != null;
	}

	public void run() {
		String oldHost = null;
		boolean hasEntity = false;
		boolean ssl = false;
		HttpResponse response = null;
		HttpRequest request = null;
		HttpHost httpHost = null;

		while (true) {
			try {
				request = requestParser.parse();
				log.info(request.getRequestLine().getUri());
				log.debug(request.toString());
				httpHost = RequestUtil.getHttpHost(request);
				ssl = "https".equalsIgnoreCase(httpHost.getSchemeName());
				if (Configure.getInstance().useBlockSite && !ssl){
					if (MatchUtil.match(request.getRequestLine().getUri(), Configure.getInstance().blockSites)){
						response = responseFactory.newHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, context);
						response.setEntity(new StringEntity(getErrorMSG("This site is blocked by admin :("), "UTF-8"));
						responseWriter.write(response);
						entitySerializerClient.serialize(clientOut, response, response.getEntity());
						clientOut.flush();
						response = null;
						break;
					}
				}
				if (Configure.getInstance().authenticated) {
					Header header = request
							.getFirstHeader(HttpHeaders.PROXY_AUTHORIZATION);
					if (header == null
							|| !AuthenticationUtil.authentication(header
									.getValue())) {
						response = responseFactory.newHttpResponse(
								HttpVersion.HTTP_1_1,
								HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED,
								context);
						response.addHeader(HttpHeaders.PROXY_AUTHENTICATE,
								"Basic realm=\"proxy.com\"");
						response.addHeader(HttpHeaders.CONNECTION, "Close");
						responseWriter.write(response);
						clientOut.flush();
						response = null;
						if (ssl){
							clientSocket.shutdownInput();
							clientSocket.shutdownOutput();
							clientSocket.close();
						}
						break;
					}
				}
				if (request instanceof HttpEntityEnclosingRequest) {
					((HttpEntityEnclosingRequest) request).setEntity(entityDeserializerClient.deserialize(clientIn, request));
					hasEntity = true;
				}
				processor.process(request, context);
				if (serverSocket == null
						|| !httpHost.getHostName().equalsIgnoreCase(oldHost)) {
					if (serverSocket != null) {
						serverSocket.close();
						serverSocket = null;
					}
					try {
						serverSocket = new Socket(httpHost.getHostName(),
								httpHost.getPort());
						// serverSocket.setSoTimeout(20000);
					} catch (UnknownHostException e) {
						response = responseFactory.newHttpResponse(
								HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_FOUND,
								context);
						response.addHeader(HttpHeaders.CONNECTION, "Close");
						response.setEntity(new StringEntity(getErrorMSG("HOST NOT FOUND :("), "UTF-8"));
						responseWriter.write(response);
						entitySerializerClient.serialize(clientOut, response, response.getEntity());
						clientOut.flush();
						response = null;
						break;
					}
					try {
						serverIn = new SocketInputBuffer(serverSocket, -1, params);
						serverOut = new SocketOutputBuffer(serverSocket, -1, params);
					} catch (IOException e) {
						log.error(e);
					}
					responseParser = new DefaultHttpResponseParser(serverIn, null, responseFactory, params);
					requestWriter = new HttpRequestWriter(serverOut, null, params);
					oldHost = httpHost.getHostName();
				}
				request = RequestUtil.convertRequest(request, hasEntity);
				connection = new ConnectionDetail(serverSocket.getLocalAddress()
						.getHostAddress(), serverSocket.getLocalPort(), serverSocket.getInetAddress().getHostAddress(),
						serverSocket.getPort(), request.getRequestLine()
								.getUri(), false, true);
				if (ssl) connection.ssl = true;
				httpProxy.addConnection(connection);
				if (ssl) {
					response = new BasicHttpResponse(HttpVersion.HTTP_1_1, 200,
							"Connection established");
					response.addHeader("Proxy-Agent", "Java Proxy Server");
					// response.addHeader(HttpHeaders.CONNECTION, "close");
					responseWriter.write(response);
					clientOut.flush();
					response = null;
					TransferToServer clientToServer = new TransferToServer(
							clientIn,
							serverOut);
					TransferToClient serverToClient = new TransferToClient(
							this, serverIn,
							clientOut);
					clientToServer.start();
					serverToClient.start();
					break;
				}
				requestWriter.write(request);
				serverOut.flush();
				if (hasEntity) {
					entitySerializerServer.serialize(serverOut, request, ((HttpEntityEnclosingRequest)request).getEntity());
					hasEntity = false;
				}
				serverOut.flush();
				response = responseParser.parse();
				response.setEntity(entityDeserializerServer.deserialize(serverIn, response));
				processor.process(response, context);
				responseWriter.write(response);
				clientOut.flush();
				if (response.getEntity() != null) {
					entitySerializerClient.serialize(clientOut, response, response.getEntity());
					clientOut.flush();
				}
				connection.open = false;
				httpProxy.removeConnection(connection);
				Header header = response.getFirstHeader(HttpHeaders.CONNECTION);
				if (request.getRequestLine().getProtocolVersion().lessEquals(HttpVersion.HTTP_1_0) || (header != null
						&& header.getValue().equalsIgnoreCase("Close"))) {
					//Disconnect
					break;
				}
				request = null;
				response = null;
				connection = null;
			} catch (ConnectionClosedException e) {
				 log.debug("Connection is closed");
				 log.error(e);
				break;
			} catch (ConnectException e) {
				log.debug(e);
				response = responseFactory.newHttpResponse(
						HttpVersion.HTTP_1_1, HttpStatus.SC_REQUEST_TIMEOUT,
						context);
				try {
					responseWriter.write(response);
					clientOut.flush();
				} catch (Exception e1) {
				}
				break;
			} catch (Exception e) {
				log.error(e);
				break;
			}
		}
		if (!ssl) {
			try {
				close();
				removeLastConnection();
			} catch (Exception e) {
			}
		}
	}

	public void removeLastConnection(){
		if (connection != null){
			connection.open = false;
			httpProxy.removeConnection(connection);
		}
	}
	
	public void close() throws IOException{
		if (serverSocket != null){
			serverSocket.shutdownOutput();
			serverSocket.shutdownInput();
			serverSocket.close();
		}
		if (clientSocket != null){
			clientSocket.shutdownOutput();
			clientSocket.shutdownInput();
			clientSocket.close();
		}
	}
	
	public static String streamToString(InputStream in) throws IOException{
		Scanner scanner = new Scanner(in, "UTF-8").useDelimiter("\\A");
		String ret = scanner.hasNext() ? scanner.next(): "";
		in.close();
		return ret;
	}
	
	public String getErrorMSG(String info) throws IOException {
		 return "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><title>WWW Proxy Server</title><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"></head><body><h1>"
		 + info + "</h1></body></html>";
	}
}
